package com.googlecode.wmlynar.gwtbeans.client.beans;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

/**
 * Class that represents the binding between the properties of the beans. Bean
 * properties may be forming a chain. For example binding between
 * <code>bean1.getPropertyX().getPropertyY()</code> and
 * <code>bean2.getPropertyZ()</code>.
 * 
 * @author Wojciech Mlynarczyk (wmlynar@gmail.com)
 * 
 */
public class Binding implements PropertyChangeListener {

    /**
     * The last bean in the chain.
     */
    private SourcesPropertyChangeEvents objectA;
    /**
     * Property of the last bean in the chain.
     */
    private Property propertyA;
    /**
     * Bean chain.
     */
    private SourcesPropertyChangeEvents[] objectsA;
    /**
     * Property chain.
     */
    private Property[] propertiesA;
    /**
     * Helper object used for rebinding if there is more than one property in
     * the chain.
     */
    private BindingHelper helperA;
    /**
     * Converts value to A format, for example if A is a String it would be
     * ToStringConverter.
     */
    private Converter converterToA;

    /**
     * The last bean in the chain.
     */
    private SourcesPropertyChangeEvents objectB;
    /**
     * Property of the last bean in the chain.
     */
    private Property propertyB;
    /**
     * Bean chain.
     */
    private SourcesPropertyChangeEvents[] objectsB;
    /**
     * Property chain.
     */
    private Property[] propertiesB;
    /**
     * Helper object used for rebinding if there is more than one property in
     * the chain.
     */
    private BindingHelper helperB;
    /**
     * Converts value to B format, for example if B is a String it would be
     * ToStringConverter.
     */
    private Converter converterToB;

    /**
     * Constructs the class.
     * 
     * @param objectA
     *            Bean A to be bound.
     * @param propertiesA
     *            Property chain of the bean A.
     * @param objectB
     *            Bean B to be bound.
     * @param propertiesB
     *            Property chain of the bean B.
     */
    public Binding(final SourcesPropertyChangeEvents objectA, final Property[] propertiesA,
            final SourcesPropertyChangeEvents objectB, final Property[] propertiesB) {
        init(objectA, propertiesA, objectB, propertiesB);
    }

    /**
     * Constructs the class.
     * 
     * @param objectA
     *            Bean A to be bound.
     * @param propertiesA
     *            Property chain of the bean A.
     * @param converterToA
     *            Converter, converting value to a format of last property in
     *            chain A.
     * @param objectB
     *            Bean B to be bound.
     * @param propertiesB
     *            Property chain of the bean B.
     * @param converterToB
     *            Converter, converting value to a format of last property in
     *            chain B.
     */
    public Binding(final SourcesPropertyChangeEvents objectA, final Property[] propertiesA,
            final Converter converterToA, final SourcesPropertyChangeEvents objectB,
            final Property[] propertiesB, final Converter converterToB) {
        init(objectA, propertiesA, objectB, propertiesB);
        this.converterToA = converterToA;
        this.converterToB = converterToB;
    }

    /**
     * Copies the property value between the last beans in the chain, from A to
     * B. To be used when objects are bound.
     */
    public void copyAToB() {
        if (objectA != null && objectB != null) {
            Object value = propertyA.getValue(objectA);
            if (converterToB != null) {
                value = converterToB.convert(value);
            }
            propertyB.setValue(objectB, value);
        }
    }

    /**
     * Copies the property value between the last beans in the chain, from B to
     * A. To be used when objects are bound.
     */
    public void copyBToA() {
        if (objectA != null && objectB != null) {
            Object value = propertyB.getValue(objectB);
            if (converterToA != null) {
                value = converterToA.convert(value);
            }
            propertyA.setValue(objectA, value);
        }
    }

    /**
     * Binds the beans.
     */
    public void bind() {
        if (helperA != null) {
            helperA.bind(0);
            objectA = objectsA[objectsA.length - 1];
        }
        if (helperB != null) {
            helperB.bind(0);
            objectB = objectsB[objectsB.length - 1];
        }
        addListeners();
    }

    /**
     * Unbinds the beans.
     */
    public void unbind() {
        removeListeners();
        if (helperA != null) {
            helperA.unbind(0);
        }
        if (helperB != null) {
            helperB.unbind(0);
        }
    }

    /**
     * Event handler, handling the copying of property values between the last
     * beans in the chain, when change is detected.
     */
    public void propertyChange(final PropertyChangeEvent evt) {
        if (evt.getSource() == objectA && propertyA.getName().equals(evt.getPropertyName())) {
            Object value = evt.getNewValue();
            if (converterToB != null) {
                value = converterToB.convert(value);
            }
            if (!equals2(propertyB.getValue(objectB), value)) {
                propertyB.setValue(objectB, value);
            }
        } else if (evt.getSource() == objectB && propertyB.getName().equals(evt.getPropertyName())) {
            Object value = evt.getNewValue();
            if (converterToA != null) {
                value = converterToA.convert(value);
            }
            if (!equals2(propertyA.getValue(objectA), value)) {
                propertyA.setValue(objectA, value);
            }
        }
    }

    /**
     * Rebinds beans in the chain of the properties of bean A, starting from
     * i-th bean in the chain.
     */
    private void rebindA(final int i) {
        removeListeners();
        if (helperA != null) {
            helperA.unbind(i);
            helperA.bind(i);
            objectA = objectsA[objectsA.length - 1];
        }
        addListeners();
        copyAToB();
    }

    /**
     * Rebinds beans in the chain of the properties of the bean B, starting from
     * i-th bean in the chain.
     */
    private void rebindB(final int i) {
        removeListeners();
        if (helperB != null) {
            helperB.unbind(i);
            helperB.bind(i);
            objectB = objectsB[objectsB.length - 1];
        }
        addListeners();
        copyBToA();
    }

    /**
     * Add listeners to the last beans in the chain.
     */
    private void addListeners() {
        if (objectA != null && objectB != null) {
            objectA.addPropertyChangeListener(propertyA.getName(), this);
            objectB.addPropertyChangeListener(propertyB.getName(), this);
        }
    }

    /**
     * Remove listeners from the last beans in the chain.
     */
    private void removeListeners() {
        if (objectA != null && objectB != null) {
            objectA.removePropertyChangeListener(propertyA.getName(), this);
            objectB.removePropertyChangeListener(propertyB.getName(), this);
        }
    }

    /**
     * Initializes variables.
     */
    private void init(final SourcesPropertyChangeEvents objectA, final Property[] propertiesA,
            final SourcesPropertyChangeEvents objectB, final Property[] propertiesB) {
        propertyA = propertiesA[propertiesA.length - 1];
        if (propertiesA.length > 1) {
            objectsA = new SourcesPropertyChangeEvents[propertiesA.length];
            objectsA[0] = objectA;
            helperA = new BindingHelper(objectsA, propertiesA);
        } else {
            this.objectA = objectA;
        }
        propertyB = propertiesB[propertiesB.length - 1];
        if (propertiesB.length > 1) {
            objectsB = new SourcesPropertyChangeEvents[propertiesB.length];
            objectsB[0] = objectB;
            helperB = new BindingHelper(objectsB, propertiesB);
        } else {
            this.objectB = objectB;
        }
    }

    /**
     * Helper function comparing two objects.
     */
    private static boolean equals2(final Object object, final Object object2) {
        if (object == null && object2 == null) {
            return true;
        }
        if (object == null && object2 != null) {
            return false;
        }
        return object.equals(object2);
    }

    /**
     * Helper class used for registering to property change event of beans in
     * middle of the chain. BinderHelper is always binded to all the beans
     * except the last bean in the chain. Property change in the intermediate
     * bean should cause rebinding of all the beans further in the chain.
     */
    private class BindingHelper implements PropertyChangeListener {
        SourcesPropertyChangeEvents[] objects;
        Property[] properties;

        public BindingHelper(final SourcesPropertyChangeEvents[] objects,
                final Property[] properties) {
            this.objects = objects;
            this.properties = properties;
        }

        public void propertyChange(final PropertyChangeEvent evt) {
            int i;
            for (i = 0; i < properties.length - 1; i++) {
                if (objects[i] == null) {
                    return;
                }
                if (objects[i] == evt.getSource()
                        && properties[i].getName().equals(evt.getPropertyName())) {
                    if (objects == objectsA) {
                        rebindA(i + 1);
                    } else {
                        rebindB(i + 1);
                    }
                    return;
                }
            }
        }

        /**
         * Binds intermediate beans to BinderHelper, starting from bean at level
         * <code>i</code>. If level is greater than <code>0</code> first the
         * bean is obtained from the the property value of the previous bean in
         * the chain. BinderHelper will be bound to all the beans except the
         * last bean in the chain.
         * 
         * @param i
         *            Level of bean depth at which to start binding the
         *            BinderHelper.
         */
        public void bind(int i) {
            if (i == 0) {
                // if the root bean then just add property change handler
                objects[i].addPropertyChangeListener(properties[i].getName(), this);
            } else {
                // else first copy the bean from the property of previous bean in the chain.
                i--;
            }
            while (true) {
                if ((objects[i + 1] = (SourcesPropertyChangeEvents) properties[i]
                        .getValue(objects[i])) == null
                        || ++i >= properties.length - 1) {
                    return;
                }
                objects[i].addPropertyChangeListener(properties[i].getName(), this);
            }
        }

        /**
         * Unbinds BinderHelper from intermediate beans, starting from bean at
         * level <code>i</code>.
         * 
         * @param i
         *            Level of bean depth at which to start unbinding the
         *            BinderHelper.
         */
        public void unbind(int i) {
            for (; i < properties.length - 1; i++) {
                if (objects[i] == null) {
                    return;
                }
                objects[i].removePropertyChangeListener(properties[i].getName(), this);
                objects[i] = null;
            }
        }
    }
}
